<?php

// vim: expandtab shiftwidth=2 softtabstop=2 tabstop=2

/**
 * Group of functions to operations on paths
 *
 * PHP version 5
 *
 * @category  Namespaces
 * @package   mPack
 * @author    Marcin Sztolcman <marcin /at/ urzenia /dot/ net>
 * @copyright 2006 Marcin Sztolcman
 * @license   GPL v.2
 * @version   SVN: $Id: ns_path.php 29 2008-01-20 23:15:20Z urzenia $
 * @link      http://m-pack.googlecode.com
 */

/**
 * Group of functions to operations on paths
 *
 * Error codes:
 * 140 InvalidArgumentException : 'Directory "%s" not found.
 *
 * @category  Namespaces
 * @package   mPack
 * @author    Marcin Sztolcman <marcin /at/ urzenia /dot/ net>
 * @copyright 2006 Marcin Sztolcman
 * @license   GPL v.2
 * @version   SVN: $Id: ns_path.php 29 2008-01-20 23:15:20Z urzenia $
 * @link      http://m-pack.googlecode.com
 */
abstract class Path
{
  /**
   * Same as DIRECTORY_SEPARATOR
   */
  const DS = DIRECTORY_SEPARATOR;

  /**
   * Same as PATH_SEPARATOR
   */
  const PS = PATH_SEPARATOR;

  /**
   * Sort order - asc.
   */
  const SORT_ASC  = 'asc';

  /**
   * Sort order - desc.
   */
  const SORT_DESC = 'desc';

  /**
   * Array for mapping sort parameters to functions
   *
   * @var array
   * @access private
   */
  private static $_sortFun = array(
    self::SORT_ASC  => 'sort',
    self::SORT_DESC => 'rsort',
  );

  /**
   * Open directory and return $class instance of $dir
   *
   * @param mixed $dir directory name or $class instance
   * @param mixed $class
   *
   * @return mixed $class object
   * @throws InvalidArgumentException
   * @access private
   * @static
   */
  private static function _checkDir($dir, $class)
  {
    if (is_string($dir)) {
      return new $class($dir);
    } else if ($dir instanceof $class) {
      return $dir;
    } else {
      throw new InvalidArgumentException('Passed directory name is not valid ' .
        'directory name or DirectoryIterator resource.', XXX);
    }
  }

  /**
   * Get join aguments into path.
   *
   * Will join all elements, they can be an arrays ({@link Arrays::flat()} is used)
   * or single arguments.
   *
   * @param mixed ,... strings or arrays
   *
   * @return string
   * @access public
   * @static
   */
  public static function join()
  {
    $argv = Arrays::flat(func_get_args());

    $path = array();
    foreach ($argv as $c) {
      $c = self::split($c);
      if (preg_match('#^[a-z]:$#', $c[0])) {
        $path = array();
      }
      $path = array_merge($path, $c);
    }

    return implode(self::DS, $path);
  }

  /**
   * Return array with path elements.
   *
   * @param string $path
   *
   * @return array
   * @access public
   * @static
   */
  public static function split($path)
  {
    $path = self::normalize($path);

    $ret = array();
    // we cut drive letter if os == windows and put it as first element
    if ('\\' == self::DS && preg_match('#^([a-z]:\\\\)#i', $path, $match)) {
      $ret[] = substr($match[1], 0, 2);
      $path = str_replace($match[1], '', $path);
    }

    return array_merge($ret, explode(self::DS, $path));
  }

  /**
   * Replace [back]slashes on correct separator, and normalize case.
   *
   * Case normalizing in only on Windows&trade;.
   *
   * @param string $path
   *
   * @return string
   * @access public
   * @static
   */
  public static function normalize($path)
  {
    static $p = array('#\\\\#', '#/#');
    static $r = array('/', self::DS);
    $path = preg_replace($p, $r, $path);

    if ('\\' == self::DS) { //windows
      $path = strtolower($path);
    }
    return $path;
  }

  /**
   * Normalizing path.
   *
   * Will remove any single and double dots ('.' and '..') with proper
   * path elements.
   *
   * @param string $path
   *
   * @return string
   * @access public
   * @static
   */
  public static function real($path)
  {
    $path = self::split($path);
    $newpath = array();
    foreach ($path as $v) {
      switch ($v) {
        case '..':
          array_pop($newpath);
        case '.' :
          continue;
        default:
          $newpath[] = $v;
      }
    }
    return self::join($newpath);
  }

  /**
   * Return true if path exists.
   *
   * @param string $path
   *
   * @return boolean
   * @access public
   * @static
   */
  public static function exists($path)
  {
    return ('' != realpath($path));
  }

  /**
   * Returns content of directory.
   *
   * Return 2-element associative array, with keys: 'files'
   * and 'dirs'.
   *
   * Return doesn't include 'dots' ('.' and '..').
   *
   * @param string  $dir   directory to scan
   * @param string  $order Path::SORT_ASC or Path::SORT_DESC
   *
   * @return array
   * @throws InvalidArgumentException
   * @access public
   * @static
   */
  public static function listDirNoSplit($dir, $order=self::SORT_ASC)
  {
    $it = self::_checkDir($dir, 'DirectoryIterator');

    $ret = array();
    foreach ($it as $obj) {
      if (!$obj->isDot()) {
        $ret[] = $obj->getFilename();
      }
    }

    $fun = self::$_sortFun[$order];
    $fun($ret, SORT_STRING);
    return $ret;
  }

  /**
   * Returns content of directory.
   *
   * Returns flat array with mixed files and directories - childs of $dir.
   *
   * Return doesn't include 'dots' ('.' and '..').
   *
   * @param string $dir directory to scan
   * @param string $order Path::SORT_ASC or Path::SORT_DESC
   *
   * @return array
   * @throws InvalidArgumentException
   * @access public
   * @static
   */
  public static function listDirSplit($dir, $order=self::SORT_ASC)
  {
    $it = self::_checkDir($dir, 'DirectoryIterator');

    $d = $f = array();

    foreach ($it as $obj) {
      if ($obj->isFile()) {
        $f[] = $obj->getFilename();
      } else if ($obj->isDir() && !$obj->isDot()) {
        $d[] = $obj->getFilename();
      }
    }

    $fun = self::$_sortFun[$order];
    $fun($d, SORT_STRING);
    $fun($f, SORT_STRING);
    return array('dirs'=>$d, 'files'=>$f);
  }

  /**
   * Returns content of directory.
   *
   * If $split == true, Path::listdir returns flat array with mixed
   * files and directories.
   * Otherwise, return 2-element associative array, with keys: 'files'
   * and 'dirs'.
   *
   * Return doesn't include 'dots' ('.' and '..').
   *
   * @param string $dir directory to scan
   * @param string $order Path::SORT_ASC or Path::SORT_DESC
   * @param boolean $split
   *
   * @return array
   * @throws InvalidArgumentException
   * @access public
   * @static
   */
  public static function listDir($dir, $order=self::SORT_ASC, $split=false)
  {
    if ($split) {
      return self::listDirSplit($dir, $order);
    } else {
      return self::listDirNoSplit($dir, $order);
    }
  }

  /**
   * Recursive walk into directory and return it's content.
   *
   * Return associative array with full path as key.
   *
   * @param string  $dir directory to scan
   * @param integer $maxLevel how deep scan $dir
   * @param string $dirFun function to execute with every directory name as parameter
   * @param string $fileFun function to execute with every file name as parameter
   * @param integer $curLevel {@internal}
   *
   * @return array
   * @throws InvalidArgumentException {@link Path::listdir()}
   * @access public
   * @static
   */
  public static function walkAssoc($dir, $maxLevel=false, $dirFun=null, $fileFun=null, $curLevel=1)
  {
    $it = self::_checkDir($dir, 'RecursiveDirectoryIterator');

    $ret = array();
    $path = self::normalize($it->getPath());

    $ret[$path] = array(
      'dirs'  => array(),
      'files' => array(),
    );
    if (false !== $maxLevel && $maxLevel < $curLevel) {
      return $ret;
    }

    foreach ($it as $obj) {
      $fname = self::normalize($obj->getFilename());

      if ($obj->isFile()) {
        $ret[$path]['files'][] = $fname;
        if (!is_null($fileFun) && is_callable($fileFun)) {
          call_user_func_array($fileFun, array($fname));
        }
      } else if ($obj->isDir() && !$it->isDot()) {
        $ret[$path]['dirs'][] = $fname;
        if (!is_null($dirFun) && is_callable($dirFun)) {
          call_user_func_array($dirFun, array($fname));
        }

        try {
          $ret = array_merge($ret, self::walkAssoc($it->getChildren(), $maxLevel, $dirFun, $fileFun, $curLevel+1));
        } catch (RuntimeException $e) {
          $ret[$fname] = array('dirs'=>array(), 'files'=>array());
        }
      }
    }

    sort($ret[$path]['files'], SORT_STRING);
    sort($ret[$path]['dirs'],  SORT_STRING);

    return $ret;
  }

  /**
   * Recursive walk into directory and return it's content.
   *
   * Return array of arrays with 3 items:
   * * full path
   * * array of directories
   * * array of files
   *
   * @param string  $dir directory to scan
   * @param integer $maxLevel how deep scan $dir
   * @param string $dirFun function to execute with every directory name as parameter
   * @param string $fileFun function to execute with every file name as parameter
   * @param integer $curLevel {@internal}
   *
   * @return array
   * @throws InvalidArgumentException {@link Path::listdir()}
   * @access public
   * @static
   */
  public static function walkNoAssoc($dir, $maxLevel=false, $dirFun=null, $fileFun=null, $curLevel=1)
  {
    $it = self::_checkDir($dir, 'RecursiveDirectoryIterator');

    $ret = $f = $d = array();
    $path = self::normalize($it->getPath());

    if (false !== $maxLevel && $maxLevel < $curLevel) {
      return array(array($path, array(), array()));
    }

    foreach ($it as $obj) {
      $fname = self::normalize($obj->getFilename());

      if ($obj->isFile()) {
        $f[] = $fname;
        if (!is_null($fileFun) && is_callable($fileFun)) {
          call_user_func_array($fileFun, array($fname));
        }

      } else if ($obj->isDir() && !$it->isDot()) {
        $d[] = $fname;
        if (!is_null($dirFun) && is_callable($dirFun)) {
          call_user_func_array($dirFun, array($fname));
        }

        try {
          $ret = array_merge($ret, self::walkNoAssoc($it->getChildren(), $maxLevel, $dirFun, $fileFun, $curLevel+1));
        } catch (RuntimeException $e) {
          $ret[] = array($fname, array(), array());
        }
      }
    }

    sort($d, SORT_STRING);
    sort($f, SORT_STRING);
    array_unshift($ret, array($path, $d, $f,));
    return $ret;
  }

  /**
   * Recursive walk into directory and return it's content.
   *
   * @param string $dir directory to scan
   * @param boolean $assoc return associative or normal array
   * @param integer $maxLevel how deep scan $dir
   * @param string $dirFun function to execute with every directory name as parameter
   * @param string $fileFun function to execute with every file name as parameter
   *
   * @return array
   * @access public
   * @static
 */
  public static function walk($dir, $assoc=false, $maxLevel=false, $dirFun=null, $fileFun=null)
  {
    if ($assoc) {
      return self::walkAssoc($dir, $maxLevel, $dirFun, $fileFun);
    } else {
      return self::walkNoAssoc($dir, $maxLevel, $dirFun, $fileFun);
    }
  }
}

?>
